<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="shortcut icon" type="image/png" href="https://xiangyuecn.gitee.io/areacity-jsspider-statsgov/assets/icon.png">

<title>ECharts Map四级下钻在线测试和预览+代码生成</title>

<script>
/*
界面显示创意来自：
	https://github.com/TangSY/echarts-map-demo
	https://www.jianshu.com/p/028525cbd080
*/
</script>

<script src="geo-echarts.js"></script>
<script src="https://jiebian.life/static/bjc/web/jiebian/www/lib_pc.js"></script>
<script src="https://xiangyuecn.gitee.io/recorder/assets/ztest-codemirror.min.5.48.4.js"></script>

<script src="geo-echarts-data-cache-0.js"></script>
</head>

<body>
<div class="lowB" style="font-size: 50px;text-align: center;color: #f60;">本工具未支持low b浏览器，换个吧&#x1f644;</div><script>document.querySelector(".lowB").innerHTML=``;</script>

<style class="defaultViewCss">
body{
	word-wrap: break-word;
	background:#f5f5f5 center top no-repeat;
	background-size: auto 680px;
	
	font-size:16px;
}

.main{
	margin:0 auto;
	padding:0 50px 80px;
}
</style>
<style>
pre{
	white-space:pre-wrap;
}
a{
	text-decoration: none;
	color:#06c;
}
a:hover{
	color:#f00;
}
p{
	text-indent: 32px;
}
img{
	max-width:90%;
}

.Center{
	text-align: center;
}


.mainBox{
	margin-top:12px;
	padding: 12px;
	border-radius: 6px;
	background: #fff;
	--border: 1px solid #f60;
	box-shadow: 2px 2px 3px #aaa;
}

.lb{
	display:inline-block;
	vertical-align: middle;
	background:#00940e;
	color:#fff;
	font-size:14px;
	padding:2px 8px;
	border-radius: 99px;
}

.pd{
	padding:0 0 6px 0;
}
.del{
	text-decoration: line-through;
}
.b{
	font-weight: bold;
	font-style: normal;
}
.i{
	padding: 2px 4px;
	background-color: #f6f6f6;
	vertical-align: middle;
	color: #c7254e;
	font-size: 12px;
	white-space: pre-wrap;
	border-radius: 3px;
	font-style: normal;
}
</style>
<script>
var IsWidget=GetArgs("widget");
if(IsWidget){
	$(".defaultViewCss").remove();
	$("body").append(`
<style>
body{
	margin:0
}
body::-webkit-scrollbar{
	width: 6px;
}
body::-webkit-scrollbar-thumb{
	border-radius: 5px;
	background-color: rgba(144, 146, 152, 0.3);
}
</style>`);
}else if(GeoECharts.IsMobile){
	$("body").append(`
<style>
.main{
	padding:0 5px 80px;
}
</style>`);
}
</script>




<div class="main">
	<div class="mainBox" style2="display:none">
		<span style="font-size:32px;color:#0B0;">ECharts Map四级下钻在线测试和预览+代码生成</span>
		<a href="..">首页</a> |
		<a href="https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov">GitHub >></a> |
		<a href="https://gitee.com/xiangyuecn/AreaCity-JsSpider-StatsGov">Gitee >></a>
	</div>
	<div style="display:none" style2="text-align: center;">
		<span style="font-size:18px;color:#fff;">坐标边界范围在线测试预览</span>
		<a href="geo-echarts.html" target="_blank">[全屏]</a>
	</div>
	
	<div class="mainBox" class2="">
		<div class="FlexBox" class3="">
			<div class="FlexItem" class2="Hide2">
				<div class="mapView"></div>
			</div>
			<div class="FlexItem" style="border:1px solid #ddd" style2="">
				<div class="echartsView"></div>
			</div>
		</div>
	</div>
	
	<div class="mainBox" class2="" style2="color:#fff">
		<div class="statusView" style="text-align:right" style2=""></div>
		
		<div style="text-align:right" style2="">
			<span class="ILB pd" style="margin-right:10px">
				<span class="lb">边界采样数 :</span>
				最多<input class="polygonPointsMax" style="text-align:center;width:60px">个坐标点
			</span>
			
			<span class="ILB pd" style="margin-right:10px">
				<span class="lb">配置参数 :</span>
				<label><input type="checkbox" class="polygon4603View">显示三沙市</label>
			</span>
			
			<span class="ILB pd">
				<span class="Btn" onclick="updateSet()">确定</span>
			</span>
		</div>
		
		<div style="padding-top:15px;">
			因为api接口返回的边界数据已经过了重新采样抽稀处理+GeoZip压缩（为了节省流量）；抽稀后最高的从5万个坐标点变成1200个坐标点，两个边界之间放大后会明显感受到不够精细，自己使用时可以调高采样数或者直接返回原始数据，就没有这个问题了。
		</div>
	</div>
	
	<div class="mainBox">
		<div class="pd">
			<span style="font-weight:bold;color:#0b0;">诸位大佬高抬贵手，</span>
			<span style="font-weight:bold;color:#fa0;font-size:30px">请勿采集本页面接口数据</span>（服务器小水管不经揍），所有数据均已提供完整版下载，并且下载的csv文件可使用转换工具转成 shp、geojson、sql、导入数据、坐标系转换 等强大支持，请直接下载数据不要采集：
		</div>
		
		<div class="pd">
			省市区三级的数据是开源的，<span style="font-weight:bold;color:#0b0;">
			<a href="download.html" target="_blank">可以到 GitHub 下载</a>
			ok_geo.csv 数据文件，里面包含了全部坐标边界数据</span>。
		</div>
		
		<div class="pd" style="font-size:18px;padding-top:20px">
			<span style="color:red;">
			乡镇这级的数据是付费数据，此页面接口返回的数据仅有最后一个乡镇是最高80个采样数的粗略边界，其他的乡镇均为边界外接矩形（4个坐标点），</span>
			<br><span style="font-weight:bold;color:#fa0;font-size:30px">采之无用！谢谢！！</span>
			完整乡镇数据文件下载：
		</div>
		
		<div style="font-size:22px;font-weight:bold;color:#0b0;">
			乡镇第4级坐标+边界范围数据
			<a href="geo-level4.html" target="_blank">可以到此下载 和 查看报价</a>
			，压缩包内 ok_geo4_*.csv 文件含了全部的乡镇数据，为付费数据，下载的压缩包里面提供了部分免费数据供测试，欢迎下载体验。
		</div>
	</div>
	
	<div class="mainBox">
		<div class="FlexBox" class2="" class3="">
			<div class="FlexItem">
				<div style="font-size:26px;font-weight:bold;padding-bottom:10px">
					ECharts Map四级下钻核心源码（前端）
				</div>
				<div style="border:1px solid #ddd">
					<textarea class="codeEditJs" style="width:98%;height:300px"></textarea>
				</div>
			</div>
			
			<div style="width:40px" style2=""></div>
			<div style="width:460px" style2="">
				<div style="font-size:26px;font-weight:bold;padding-bottom:10px">
					Geo WKT数据拉取接口（后端 C#）
				</div>
				<div style="border:1px solid #ddd">
					<textarea class="codeEditCs" style="width:98%;height:300px"></textarea>
				</div>
			</div>
		</div>
	</div>
	
</div>


<script>
var Review=function(){
	var s,c;
	if(IsWidget){
		s="style2";
		c="class2";
	}else if(GeoECharts.IsMobile){
		s="style3";
		c="class3";
	};
	if(s){
		var ls=$("["+s+"]");
		for(var i=0;i<ls.length;i++){
			var el=$(ls[i]);
			el.attr("style", el.attr(s));
		}
		var ls=$("["+c+"]");
		for(var i=0;i<ls.length;i++){
			var el=$(ls[i]);
			el.attr("class", el.attr(c));
		}
	}
};
Review();

$(function(){
	var elem=$(".codeEditJs");
	var w=elem.width();
	var edit=CodeMirror.fromTextArea(elem[0],{
			mode:"javascript"
			,lineNumbers:true
			,lineWrapping:true
		});
	window.CodeEditJs=edit;
	edit.setSize(w+"px","auto");

	edit.setValue(GeoECharts.GeoEChartsLib.toString());
	edit.refresh();
	
	elem=$(".codeEditCs");
	var w=elem.width();
	var edit=CodeMirror.fromTextArea(elem[0],{
			mode:"javascript"
			,lineNumbers:true
			,lineWrapping:true
		});
	window.CodeEditJs=edit;
	edit.setSize(w+"px","auto");

	edit.setValue($(".CS_Code").html().trim());
	edit.refresh();
});
</script>




<script>
var geoEChartsLib=GeoECharts.GeoEChartsLib();

$(".mapView,.echartsView").css({height:IsWidget?200:$(window).height()-130+"px"});
var map=0;
if(!IsWidget){
	LoadJs("https://webapi.amap.com/maps?v=1.4.15",function(){
		map=new AMap.Map($(".mapView")[0]);

		AMap.plugin([ 'AMap.ToolBar', 'AMap.Scale', 'AMap.MapType' ], function(){
			map.addControl(new AMap.ToolBar());
			map.addControl(new AMap.Scale());
			map.addControl(new AMap.MapType({defaultType:0, showRoad:true}));
		});
	});
};


var levels=[];
var geoECharts=geoEChartsLib.Create({
	elem:".echartsView"
	,api:localStorage["Page_GeoECharts_api"]||"https://jiebian.life/api/extend/h5/github/areacity_geo"
	,reqPost:function(url,args,True,False){
		LoadData(url,args,function(data){
			True(data.list?data:data.v);
		},False,false);
	}
	,onLoadBefore:function(args,loadProcess){
		PageModule.maskLoad();
		
		//0级数据，默认使用js缓存文件，不走接口，大幅减少api服务器流量
		if(args.id==0 && window.DataCache0){
			loadProcess(function(next){
				console.log("Geo DataCache0: "+FormatDate(DataCache0.T));
				next(DataCache0.v);
			});
			return;
		};
		
		//非0的发一个流量统计
		var img=new Image();
		img.src="https://ia.51.la/go1?id=20807127&pvFlag=1&cu="+encodeURIComponent(/(.+?:\/\/.+?\/)/.exec(location.href)[1]+"click-geo-echarts-"+args.id);
	}
	,onLoadEnd:function(err,apiData,mapDatas,geojson,dataProcess){
		PageModule.closeMask();
		if(err){
			PageModule.toast(err);
			return;
		};
		var cur=geoECharts.current;
		
		//0级时，控制一下缩放
		geoECharts.set.zoom=1;
		if(cur.id==0 && IsWidget){
			geoECharts.set.zoom=2;
		};
		
		//echarts内容控制
		if(geoEChartsLib.IsEmptyGeoJSON(geojson)){
			PageModule.toast("无任何子级边界");
		};
		var zipSize=0,rawSize=0;
		for(var i=0;i<mapDatas.length;i++){
			var o=mapDatas[i],isZip=o.polygon.indexOf("Z:")+1;
			//提示信息替换
			o.tips+="<br>【自定义】:";
			for(var j=0;j<geojson.features.length;j++){
				var f=geojson.features[j],fp=f.properties,fg=f.geometry;
				if(fp.id==o.id){
					var fi=geoEChartsLib.FeaturePolygonInfo(f);
					o.tips+="<br>类型: "+fg.type;
					o.tips+="<br>环数: "+fi.polygon;
					o.tips+="<br>坐标数: "+fi.point;
					o.tips+='<br>采样数: '+apiData._PolygonPointsMax;
					
					o.tips+='<br>'+(isZip?'GeoZip':'')+'大小: '+(o.polygon.length/1024).toFixed(2)+" KB";
					zipSize+=o.polygon.length;
					if(isZip){
						o.tips+="<br>解压大小≈ "+(fi.point*21/1024).toFixed(2)+" KB";
						rawSize+=fi.point*21;
					}
				}
			}
		};
		if(isZip){
			console.log("zipSize: "+(zipSize/1024).toFixed(2)+" KB");
		}
		console.log("rawSize≈ "+((rawSize||zipSize)/1024).toFixed(2)+" KB");
		
		//显示路径
		levels.length=cur.level;
		levels.push(cur);
		
		var html=[];
		for(var i=0;i<levels.length;i++){
			var o=levels[i];
			html.push('<a onclick="goLevel('+i+')">'+o.name+'</a>');
		};
		var tips="路径："+html.join(" > ");
		
		if(cur.level==3 && !/^(8|4420|4403)/.test(cur.id)){
			var msg='当前为乡镇级别边界（付费数据），只有是最后一个乡镇【'+mapDatas[mapDatas.length-1].name+'】是最高80个采样数的粗略边界，其他的乡镇均为边界外接矩形（4个坐标点），仅供测试（深圳、中山、香港、澳门无此限制）';
			PageModule.toast(msg);
			
			tips+='<div style="color:#fa0">'+msg+'，如需完整的乡镇边界数据，<a href="geo-level4.html" target="_blank">可以到此下载和购买</a>。</div>';
		}
		$(".statusView").html(tips);
		
		
		//绘制到地图中
		if(map){
		
			map.clearMap();
			//https://developer.amap.com/demo/javascript-api/example/overlayers/geojson
			var texts={};
			var mapGeo = new AMap.GeoJSON({
				geoJSON: geojson
				,getPolygon: function(json, lnglats) {
					var pol=new AMap.Polygon({
						path: lnglats,
						strokeWeight: 1,
						strokeColor: '#0091ea',
						fillColor: '#80d8ff',
						fillOpacity: 0.3
					});
					
					if(lnglats.length){
						var prop=json.properties;
						prop=prop._parentProperities||prop;
						var obj=texts[prop.id]||{pols:[]};
						obj.name=prop.name;
						obj.data=prop;
						obj.pols.push({
							pos:pol.getBounds().getCenter()
							,size:pol.getArea()
						});
						texts[prop.id]=obj;
					}
					
					return pol;
				}
			});
			mapGeo.setMap(map);
			
			//显示名称
			for(var k in texts){
				var o=texts[k];
				o.pols.sort(function(a,b){return b.size-a.size});
				var txt=new AMap.Text({
					map:map
					,text:o.name
					,position:o.pols[0].pos
					,style:{
						color:"#06c"
						,border:"none"
						,background:"none"
						,fontSize:"14px"
					}
					,extData:o.data
				});
				txt.on("click",function(e){
					var o=e.target.getExtData();
					geoECharts.load(o);
				});
			}
			
			map.setFitView();
		}
	}
});

var goLevel=function(idx){
	geoECharts.load(levels[idx]);
};

var updateSet=function(){
	geoEChartsLib.Polygon4603Hide=!$(".polygon4603View")[0].checked;
	
	var pmax=+$(".polygonPointsMax").val()||600;
	$(".polygonPointsMax").val(pmax);
	if(pmax>1200){
		pmax=1200;
		$(".polygonPointsMax").val(pmax);
		
		PageModule.toast("错误：由于测试服务器带宽太小，边界采样数限制最高1200，如需体验原版边界数据效果，请自行提供接口测试");
		return;
	}
	geoECharts.set.polygonPointsMax=pmax;
	
	geoECharts.reload();
};
updateSet();
</script>














<script class="CS_Code" type="text/template">
/*************************
这个源码就是本页面用到的api接口源码（C#），贴出来主要是方便大家借鉴，直接copy是无法使用的。

【禁】 > 源码都让你看了，请不要搞我的服务器哦~ <


【WKT】mysql、sqlserver直接查询出来的 polygon 一般是二进制格式，需要使用数据库内置的 ST_AsText 方法转换成 WKT 文本；sql查询语句：
[MySQL] select id,ext_path,ST_AsText(polygon) as polygon from AreaCity_Geo where id=11
[MSSQL] select id,ext_path,polygon.STAsText() as polygon from AreaCity_Geo where id=11


【采样抽稀】省市区三级边界数据，一半以上边界的坐标点超过1000个点，超过1万个坐标点边界有80个，超过3万个坐标点的边界有11个；内蒙的边界坐标点数最多（5万个点），转成文本后超过1MB大小，重采样成600个点后变成16KB大小；在大部分ECharts显示场合，重采样抽稀后边界外观基本上是一致的（下面源码中有一段边界质量优化的代码对超大的边界进行了增强处理），数据量已经大幅减少。

【GeoZip压缩、解压】本代码内实现了一套压缩解压的代码，6位小数精度下可压缩到 1/3 - 1/5 大小。压缩代码参考自echarts（ZigZag算法）：https://github.com/apache/echarts/blob/8eeb7e5abe207d0536c62ce1f4ddecc6adfdf85e/src/util/mapData/rawData/encode.js
*************************/



	/// <summary>
	/// GitHub测试页面 ECharts Map四级下钻需要用到的边界拉取接口
	/// AreaCity-JsSpider-StatsGov
	/// </summary>
	public class areacity_geo {

		static private readonly Dictionary<string, Dictionary<string, object>> Cache = new Dictionary<string, Dictionary<string, object>>();

		static public void main() {
			var ctx = ServiceContext.Current;
			var req = RequestContext.Current;

			string refHost = "";
			if (req.Context.RequestReferrer != null) {
				refHost = req.Context.RequestReferrer.Host;
			}
			if (Exp_Host.IsMatch(refHost)) {
				req.Context.ResponseHead("Access-Control-Allow-Origin", "*");
			} else {
				ctx.error("当前页面不可以调用本接口");
				return;
			}



			var rtvData = new Dictionary<string, object>();
			ctx.Value = rtvData;

			var pid = req.getInt("id");
			var level = req.getInt("level");
			var polygonPointsMax = req.getInt("polygonPointsMax");

			polygonPointsMax = Math.Max(polygonPointsMax, 100);
			if (polygonPointsMax > 1200) {
				ctx.error("边界采样数超过限制");
				return;
			}


			var useCache = polygonPointsMax == 600;
			if (level == 3) {
				polygonPointsMax = 80;
				useCache = true;
			}
			rtvData["_PolygonPointsMax"] = polygonPointsMax;
			rtvData["_Pid"] = pid;

			if (useCache) {
				var cache = Cache.getOrNull(pid + "");
				if (cache != null) {
					long time = cache.getLong("time");
					if (Unit.GetMS() - time < 24 * 60 * 60 * 1000) {
						rtvData["list"] = cache.getOrNull("list");
						rtvData["_Cache"] = time;
						return;
					}
				}
			}



			List<Dictionary<string, object>> dbList;
			if (level == 0 || level == 1 || level == 2) {

				//省市区边界
				FieldRead FR = DBTable.AreaCityGeo.FRReadOrWrite();
				FR.AddInt("id");
				FR.AddString("ext_path");
				FR.AddString("polygon", "polygon.STAsText()", "", true);
				dbList = FR.QueryAll(new SQL().Cmd("pid=").Int(pid));

			} else if (level == 3) {

				//乡镇边界，最高80个点，数据库中只有最后一个是有边界数据，其他的都是外接矩形框
				FieldRead FR = DBTable.AreaCityGeo4.FRReadOrWrite();
				FR.AddInt("id", "unique_id");
				FR.AddString("ext_path");
				FR.AddString("polygon", "polygon.STAsText()", "", true);
				dbList = FR.QueryAll(new SQL().Cmd("pid=").Int(pid));

			} else {
				ctx.error("level值" + level + "无效");
				return;
			}

			List<Dictionary<string, object>> list = new List<Dictionary<string, object>>();
			rtvData["list"] = list;
			foreach (var item in dbList) {
				Dictionary<string, object> obj = new Dictionary<string, object>();
				list.Add(obj);

				obj["id"] = item.getOrNull("id");
				obj["ext_path"] = item.getOrNull("ext_path");
				obj["polygon"] = FormatPolygon(item.getString("polygon"), polygonPointsMax);
			}

			if (useCache) {
				Dictionary<string, object> cache = new Dictionary<string, object>();
				cache["time"] = Unit.GetMS();
				cache["list"] = list;
				Cache[pid + ""] = cache;
			}
		}


		static private string FormatPolygon(string wkt, int pointsMax) {
			string[] wkts;
			if (wkt.Contains("EMPTY")) {
				return wkt;
			} else if (wkt.StartsWith("POLYGON")) {
				wkt = Exp_1.Replace(wkt, "");
				wkts = new string[] { wkt };
			} else if (wkt.StartsWith("MULTIPOLYGON")) {
				wkt = Exp_2.Replace(wkt, "");
				wkts = Exp_3.Split(wkt);
			} else {
				return "POLYGON EMPTY";
			}

			var multiArr = new List<List<List<double[]>>>();
			var allPs = new List<List<double[]>>();
			foreach (var pl in wkts) {
				var pls = Exp_4.Split(pl);
				var plArr = new List<List<double[]>>();
				multiArr.Add(plArr);

				foreach (var line in pls) {
					var ps = new List<string>(Exp_5.Split(line));
					var ps2 = new List<double[]>(ps.Count);
					plArr.Add(ps2);
					allPs.Add(ps2);
					foreach (var p in ps) {
						var p2 = p.Split(' ');
						var x = double.Parse(p2[0]);
						var y = double.Parse(p2[1]);
						ps2.Add(new double[] { x, y });
					}
				}
			}
			allPs.Sort((a, b) => {
				return b.Count - a.Count;
			});

			int count3j = 0;
			for (int i = 3; i < allPs.Count; i++) {
				count3j += allPs[i].Count;
			}
			for (int i = 0; i < allPs.Count; i++) {
				List<double[]> arr = allPs[i];
				int max = pointsMax;
				if (i == 0) {//首个环使用最大点数
					//NOOP
				} else if (i == 1 || i == 2) {//后两个环各使用1/3点数
					max = pointsMax / 3;
				} else {//后续的共享1/3点数，但最大可以有8个点
					max = pointsMax / 3 * arr.Count / count3j;
					max = Math.Max(max, 8);
				}
				if (arr.Count <= max) {
					continue;//无需处理
				}

				//如果是第一个环，并且点数非常多，将进行质量控制，间距过大的点优先保留，间距低的点优先删除
				bool quality = i == 0 && arr.Count > max * 10;

				double lineTotal = 0;//粗略计算边界长度
				if (quality) {
					double[] prev = null;
					for (var i2 = 0; i2 < arr.Count; i2++) {
						var cur = arr[i2];
						if (prev != null) {
							lineTotal += (Math.Abs(prev[0] - cur[0]) + Math.Abs(prev[1] - cur[1]));
						}
						prev = cur;
					}
				}
				double widthBase = lineTotal / max; //均分长度


				List<double[]> arr2 = null;
				for (var i2 = 1; i2 <= 2; i2++) {//质量处理 或者 放弃
					bool useQuality = quality && i2 != 2;//最后一次将放弃
					double widthMax = widthBase * 2;//超过这个间距的点全部保留
					double widthMin = widthBase * (0.1 * i2);//低于这个间距的点全部删除

					arr2 = new List<double[]>();

					//抽样处理
					double c = 1d * max / arr.Count;//采样率
					double pos = 0;
					int posCur = -1;
					double[] cur, prev = null;
					for (int j = 0; j < arr.Count - 1; j++) {
						cur = arr[j];
						if (useQuality && prev != null) {
							//质量控制，粗略计算间距
							var jl = (Math.Abs(prev[0] - cur[0]) + Math.Abs(prev[1] - cur[1]));
							if (jl > widthMax) {
								//间距过大的全部保留
								prev = cur;
								arr2.Add(cur);
								pos = 0;
								posCur = 0;
								continue;
							}
							if (jl < widthMin) {
								//间距过低的全部不要
								continue;
							}
						}

						pos += c;
						if (posCur != (int)pos) {
							prev = cur;
							arr2.Add(cur);
							posCur = (int)pos;
						}
					}

					if (!useQuality || arr2.Count < max * 1.5) {
						//基本达标，不用再抽了
						break;
					}
				}
				arr.Clear();
				arr.AddRange(arr2);
			}

			//还原成wkt文本
			StringBuilder str = new StringBuilder();
			if (multiArr.Count > 1) {
				str.Append("MULTIPOLYGON(");
			} else {
				str.Append("POLYGON");
			}
			for (int i = 0; i < multiArr.Count; i++) {
				var plArr = multiArr[i];
				if (i > 0) {
					str.Append(",");
				}
				str.Append("((");
				for (var j = 0; j < plArr.Count; j++) {
					var ps = plArr[j];
					if (j > 0) {
						str.Append("),(");
					}

					if (true) {
						//返回压缩后的边界数据，大幅减少流量
						ps.Add(ps[0]);//闭环
						str.Append(GeoZip(ps));
					} else { 
						//返回原始wkt数据
						for (var n = 0; n < ps.Count; n++) {
							if (n > 0) {
								str.Append(",");
							}
							str.Append(ps[n][0] + " " + ps[n][1]);
						}

						str.Append(",");
						str.Append(ps[0][0] + " " + ps[0][1]);//闭环
					}
				}
				str.Append("))");
			}
			if (multiArr.Count > 1) {
				str.Append(")");
			}
			return str.ToString();
		}
		static private readonly Regex
			Exp_1 = new Regex(@"^POLYGON\s*\(\(|\)\)$")
			, Exp_2 = new Regex(@"^MULTIPOLYGON\s*\(\(\(|\)\)\)$")
			, Exp_3 = new Regex(@"\)\)\s*,\s*\(\(")
			, Exp_4 = new Regex(@"\)\s*,\s*\(")
			, Exp_5 = new Regex(@"\s*,\s*")

			, Exp_Host = new Regex(@"(?:\.(?:gitee|github)\.io)(?:\:\d+)?$")
			;



		static private long Zip_Scale = 1000000;//最大支持6位小数精度
		/// <summary>
		/// 边界坐标压缩，返回base64字符串
		/// </summary>
		static public string GeoZip(List<double[]> points) {
			var bytes = GeoZipBytes(points);
			return "Z:" + Convert.ToBase64String(bytes);
		}
		/// <summary>
		/// 边界坐标解压，返回坐标数组
		/// </summary>
		static public List<double[]> GeoUnZip(string base64) {
			if (string.IsNullOrEmpty(base64) || base64.Length<3) {
				return new List<double[]>();
			}
			var bytes = Convert.FromBase64String(base64.Substring(2));
			return GeoUnZipBytes(bytes);
		}
		/// <summary>
		/// 边界坐标压缩，返回二进制
		/// </summary>
		static public byte[] GeoZipBytes(List<double[]> points) {
			if (points == null || points.Count < 1) {
				return new byte[0];
			}

			var point0 = points[0];
			var x = (long)Math.Ceiling(point0[0] * Zip_Scale);
			var y = (long)Math.Ceiling(point0[1] * Zip_Scale);

			var rtv = new MemoryStream();
			rtv.write(x + " " + y + ":");
			int markPos = 0, markLen = 4;
			for (var i = 1; i < points.Count; i++) {
				if (markLen == 4) {
					markLen = 0;
					markPos = (int)rtv.Length;
					rtv.WriteByte(0);
				}
				var point = points[i];
				x = _GeoZip(rtv, point[0], x, markPos, markLen++);
				y = _GeoZip(rtv, point[1], y, markPos, markLen++);
			}

			byte[] rtv2 = rtv.ToArray();
			if (true) {//测试一下压缩的精度
				var un = GeoUnZipBytes(rtv2);
				if (un.Count != points.Count) {
					throw new Exception("压缩结果还原长度不一致");
				}
				for (var i = 0; i < un.Count; i++) {
					double[] p1 = un[i], p2 = points[i];
					if (Math.Abs(p1[0] - p2[0]) > 0.000002 || Math.Abs(p1[1] - p2[1]) > 0.000002) {
						throw new Exception("压缩结果还原精度不足"+ un[i]+" - "+ points[i]);
					}
				};
			};
			return rtv2;
		}
		static private long _GeoZip(MemoryStream rtv, double cur_, long prev, int markPos, int markLen) {
			long cur = (long)Math.Ceiling(cur_ * Zip_Scale);
			long val = cur - prev;
			var mark0 = val < 0 ? 1 : 0;//符号位
			val = Math.Abs(val);
			var mark1 = val > 0xff ? 1 : 0;//单字节是否溢出

			if (mark1>0) {//溢出了，用多个字节来存储，2字节+
				if (val > 1073741823) {//parseInt('00111111'+'11111111'+'11111111'+'11111111',2)
					throw new Exception("差值超过支持范围");
				}
				//4个字节对应的byte，ps：java byte是有符号的
				byte v1 = (byte)(val & 0b00111111)
					, v2 = (byte)((val >> 6) & 0xff)
					, v3 = (byte)((val >> (6 + 8)) & 0xff)
					, v4 = (byte)((val >> (6 + 16)) & 0xff);
				//实际多用了几个字节
				var more = 0;//0就是2字节来存
				if (v4 > 0) {
					more = 2;//2+2=4字节来存
				} else if (v3 > 0) {
					more = 1;
				};
				//写入字节数
				v1 = (byte)(v1 | (more << 6));

				rtv.WriteByte(v1);
				rtv.WriteByte(v2);
				if (more > 0) rtv.WriteByte(v3);
				if (more > 1) rtv.WriteByte(v4);
			} else {//没有溢出，直接存
				rtv.WriteByte((byte)val);
			}

			//写入符号位和溢出位标识
			var bakPos = rtv.Position;
			rtv.Position = markPos;
			var mark = rtv.ReadByte();
			mark |= (mark0 | (mark1 << 1)) << (markLen * 2);

			rtv.Position = markPos;
			rtv.WriteByte((byte)mark);
			rtv.Position = bakPos;

			return cur;
		}

		/// <summary>
		/// 二进制边界坐标解压，返回坐标数组
		/// </summary>
		static public List<double[]> GeoUnZipBytes(byte[] bytes) {
			List<double[]> rtv = new List<double[]>();
			if (bytes==null || bytes.Length<1) {
				return rtv;
			}

			//先提取出第一个坐标
			string xS = "", yS = "";
			bool isY = false;
			var i = 0;
			for (; i < bytes.Length; i++) {
				var chr = bytes[i];
				if (chr == ':') {
					i++; break;
				}
				if (chr == ' ') {
					isY = true;
				} else if (isY) {
					yS += (char)chr;
				} else {
					xS += (char)chr;
				}
			};
			long x = long.Parse(xS);
			long y = long.Parse(yS);
			rtv.Add(new double[] { 1d * x / Zip_Scale, 1d * y / Zip_Scale });

			//继续，连续解码
			int markLen = 4;byte mark = 0;
			int[] refOut = new int[] { -1, 0 };
			for (; i < bytes.Length; i++) {
				if (markLen == 4) {//提取出符号+溢出标识字节
					markLen = 0;
					mark = bytes[i];
					continue;
				};
				refOut[1] = i;
				x = _GeoUnZip(bytes, refOut, x, mark, markLen++);
				y = _GeoUnZip(bytes, refOut, y, mark, markLen++);
				i = refOut[1] - 1;

				rtv.Add(new double[] { 1d * x / Zip_Scale, 1d * y / Zip_Scale });
			}
			return rtv;
		}
		static private long _GeoUnZip(byte[] bytes, int[] refOut, long prev, byte mark, int markLen) {
			var i = refOut[1];
			mark = (byte)(mark >> (markLen * 2));
			var mark0 = mark & 0b1;//符号位
			var mark1 = (mark & 0b10) >> 1;//单字节是否溢出

			var val = (int)bytes[i++];
			if (mark1>0) {//已溢出，需要读取比2字节多用了几个字节0-3
				var more = (val & 0b11000000) >> 6;
				val &= 0b111111;
				val |= bytes[i++] << 6;
				if (more > 0) val |= bytes[i++] << (6 + 8);
				if (more > 1) val |= bytes[i++] << (6 + 16);
			};
			if (mark0>0) {//负数
				val = -val;
			};

			refOut[1] = i;
			return val + prev;
		}

	}
</script>


<div style="width:0;height:0;overflow:hidden;">
	<img src="https://ia.51.la/go1?id=20807127&pvFlag=1">
</div>

</body>
</html>
